package daybook.model.transaction;

import daybook.Resources;
import daybook.exception.NotFoundException;
import daybook.io.SimpleExpression;
import daybook.model.Account;
import daybook.model.ChartOfAccounts;
import daybook.util.Utils;
import daybook.util.tree.Tree;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public final class VirtualAccount implements Predicate<TransactionEntry> {
    private static final Logger LOG = LoggerFactory.getLogger(VirtualAccount.class);
    private static final Pattern NAME = Pattern.compile("(?<account>[^\\(\\[]*)(?:\\((?<attributes>.*)\\))?\\p{Blank}*(?:\\[(?<tags>.*)\\])?");
    private static final String ACCOUNT = "account";
    private static final String ATTRIBUTES = "attributes";
    private static final String TAGS = "tags";
    private final String name;
    private final Set<Account> accounts; // null表示对账户没有限制
    private final Set<String> tags; // null表示对Tag没有限制

    private VirtualAccount(String name, Set<Account> accounts, Set<String> tags) {
        this.name = Objects.requireNonNull(name);
        this.accounts = accounts;
        this.tags = tags;
    }

    private static Tree<Account> getAccountTree(String accountName, ChartOfAccounts chartOfAccounts) {
        if (Utils.isBlank(accountName)) {
            return chartOfAccounts.getTree();
        } else {
            return chartOfAccounts.findTree(accountName)
                    .orElseThrow(() -> NotFoundException.of(Resources.ErrorMessages.ACCOUNT_NOT_FOUND, accountName));
        }
    }

    private static Set<Account> makeAccounts(Tree<Account> accountTree, Set<String> attributes) {
        if (attributes == null) {
            return  (accountTree.getValue() == Account.ROOT) ?
                    null : accountTree.leafStream().collect(Collectors.toSet());
        } else {
            return accountTree.leafStream()
                    .filter(account -> account.getAttributes().stream().anyMatch(attributes::contains))
                    .collect(Collectors.toSet());
        }
    }

    private static Set<String> toStringSet(String str) {
        if (Utils.isBlank(str)) {
            return null;
        } else {
            return Arrays.stream(str.split(","))
                    .map(String::trim)
                    .collect(Collectors.toSet());
        }
    }

    private static String normalizeName(String accountName, Set<String> attributes, Set<String> tags) {
        return (accountName == null ? "": accountName)
                + (attributes == null ? "" : attributes.stream().collect(Collectors.joining(",","(", ")")))
                + (tags == null ? "" : tags.stream().collect(Collectors.joining(",", "[", "]")));
    }

    // throws IllegalArgumentException, NotFoundException
    public static VirtualAccount of(String name, ChartOfAccounts chartOfAccounts) {
        LOG.trace(">>> VirtualAccount.of");
        if (Utils.isBlank(name)) {
            throw new IllegalArgumentException("empty virtual account name");
        }
        final Matcher m = NAME.matcher(name);
        if (!m.matches()) {
            throw new IllegalArgumentException("invalid virtual account name: " + name);
        }
        final String accountName = m.group(ACCOUNT).trim();
        final Set<String> attributes = toStringSet(m.group(ATTRIBUTES));
        final Set<String> tags = toStringSet(m.group(TAGS));
        LOG.debug("=== of: account name(or full name, or code): {}, attributes: {}, tags: {}", accountName, attributes, tags);

        final Tree<Account> accountTree = getAccountTree(accountName, chartOfAccounts);
        if (LOG.isDebugEnabled()) {
            final Account a = accountTree.getValue();
            LOG.debug("=== of: account name decided account: " + a.getName() + " (" + a.getCode() + ")");
        }

        final Set<Account> accounts = makeAccounts(accountTree, attributes);
        if (LOG.isDebugEnabled()) {
            LOG.debug("=== of: accounts: {}", SimpleExpression.accountsToString(accounts));
        }

        final String normalizedName = normalizeName(accountName, attributes, tags);
        LOG.debug("=== of: normalized name: {}", normalizedName);
        LOG.trace("<<< VirtualAccount.of");
        return new VirtualAccount(normalizedName, accounts, tags);
    }

    @Override
    public boolean test(TransactionEntry transactionEntry) {
        return (accounts == null || accounts.contains(transactionEntry.getAccount()))
                && (tags == null || !Collections.disjoint(transactionEntry.getTags(), tags));
    }

    public String getName() {
        return name;
    }

    public Optional<Set<Account>> getAccounts() {
        return Optional.ofNullable(accounts);
    }

    public Optional<Set<String>> getTags() {
        return Optional.ofNullable(tags);
    }

    // 如果VirtualAccount只限定了一个Account, 并且没有限定Tags
    // 那么该方法返回这个Account
    public Optional<Account> getSingleAccount() {
        if (tags != null || accounts == null) {
            return Optional.empty();
        } else if (accounts.size() == 1) {
            return Optional.of(accounts.iterator().next());
        } else {
            return Optional.empty();
        }
    }

    public boolean isAlwaysFalse() {
        // can tags be empty ?
        return ((accounts != null) && accounts.isEmpty())
                || ((tags != null) && tags.isEmpty());
    }

//    public boolean isAlwaysTrue() {
//        return accounts == null && tags == null;
//    }

    // 当两个VirtualAccount相同或作为Predicate相等时返回true
    public boolean similar(VirtualAccount other) {
        return (this == other) || (name.equals(other.name))
                || (Objects.equals(accounts, other.accounts) && Objects.equals(tags, other.tags))
                || (isAlwaysFalse() && other.isAlwaysFalse());
    }

    private static <T> boolean isDisjoint(Set<T> set1, Set<T> set2) {
        return (set1 == null && set2 != null && set2.isEmpty())
                || (set2 == null && set1 != null && set1.isEmpty())
                || (set1 != null && set2 != null && Collections.disjoint(set1, set2));
    }

    // 如果两个VirtualAccount作为Predicate划定的集合不相交
    // 即for all items, (this.test(item) == true) != (other.test(item) == true)
    public boolean isDisjoint(VirtualAccount other) {
        return isDisjoint(accounts, other.accounts) || isDisjoint(tags, other.tags);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        VirtualAccount that = (VirtualAccount) o;

        return name.equals(that.name);
    }

    @Override
    public int hashCode() {
        return name.hashCode();
    }

    @Override
    public String toString() {
        return "VirtualAccount{" +
                "name='" + name + '\'' +
                ", accounts=" + accounts +
                ", tags=" + tags +
                '}';
    }
}
